﻿using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Drawing;

using SharpGL.Media3D;
using SharpGL.SceneGraph.Helpers;

namespace SharpGL.SceneGraph.Primitives.SmartAxies
{
    [TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))]
    public class Axis : INotifyPropertyChanged
    {
        #region Private Constants

        private const double minIncrement = 1e-30d;

        #endregion Private Constants

        #region Private Fields

        private AxisType type;

        private bool visible = true;
        private GLColor color;
        private float width = 2.0f;
        private bool drawArrow = true;

        private AxisName name = new AxisName();
        private AxisLabels labels = new AxisLabels();
        private AxisTicks ticks = new AxisTicks();
        private AxisTicksLines ticksLines = new AxisTicksLines();

        float[] ticksValues = null;
        Point3D[] verticesBuffer = null;

        Rect3D boundingRect = Rect3D.Empty;
        Size drawRectSize = Size.Empty;

        #endregion Private Fields

        #region Private Methods

        private GLColor DefaultColor(AxisType type)
        {
            switch (type)
            {
                case AxisType.Abscissa:
                    return new GLColor(1.0f, 0.0f, 0.0f, 1.0f);
                
                case AxisType.Ordinate:
                    return new GLColor(0.0f, 1.0f, 0.0f, 1.0f);
                
                case AxisType.Applicate:
                    return new GLColor(0.0f, 0.0f, 1.0f, 1.0f);
                
                default:
                    return new GLColor(1.0f, 1.0f, 1.0f, 1.0f);
            }
        }

        private string DefaultName(AxisType type)
        {
            switch (type)
            {
                case AxisType.Abscissa:
                    return "X";
                
                case AxisType.Ordinate:
                    return "Y";
                
                case AxisType.Applicate:
                    return "Z";
                
                default:
                    return "";
            }
        }

        private void DefaultTextAlignment(AxisName name, AxisLabels labels)
        {
            switch (type)
            {
                case AxisType.Abscissa:
                    name.HorizontalAlignment = Text.HorizontalAlignment.Left;
                    name.VerticalAlignment = Text.VerticalAlignment.Center;
                    labels.HorizontalAlignment = Text.HorizontalAlignment.Center;
                    labels.VerticalAlignment = Text.VerticalAlignment.Top;
                    break;
                
                case AxisType.Ordinate:
                    name.HorizontalAlignment = Text.HorizontalAlignment.Left;
                    name.VerticalAlignment = Text.VerticalAlignment.Center;
                    labels.HorizontalAlignment = Text.HorizontalAlignment.Center;
                    labels.VerticalAlignment = Text.VerticalAlignment.Top;
                    break;
                
                case AxisType.Applicate:
                    name.HorizontalAlignment = Text.HorizontalAlignment.Center;
                    name.VerticalAlignment = Text.VerticalAlignment.Bottom;
                    labels.HorizontalAlignment = Text.HorizontalAlignment.Right;
                    labels.VerticalAlignment = Text.VerticalAlignment.Center;
                    break;
                
                default:
                    name.HorizontalAlignment = Text.HorizontalAlignment.Center;
                    name.VerticalAlignment = Text.VerticalAlignment.Center;
                    labels.HorizontalAlignment = Text.HorizontalAlignment.Center;
                    labels.VerticalAlignment = Text.VerticalAlignment.Center;
                    break;
            }
        }

        private Vector3D DefaultTicksDirection(AxisType type)
        {
            switch (type)
            {
                case AxisType.Abscissa:
                
                case AxisType.Ordinate:
                    return new Vector3D(0.0f, 0.0f, -1.0f);
                
                case AxisType.Applicate:
                    return new Vector3D(-1.0f, 0.0f, 0.0f);
                
                default:
                    return new Vector3D(0.0f, 0.0f, -1.0f);
            }
        }

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (sender is AxisTicks)
            {
                boundingRect = Rect3D.Empty;
                drawRectSize = Size.Empty;
            }

            NotifyPropertyChanged(sender.GetType() + "." + e.PropertyName);
        }

        #endregion Private Methods

        #region Protected Methods

        protected long ComputeAxisParameters(OpenGL gl, Rectangle drawRect,
                                             double minValue, double maxValue, bool roundFirstValue,
                                             Font labelsFont, string labelsFormat, float labelsSeparation,
                                             out double interval, out double startValue)
        {
            long ticksCount;
            double tmpAxisValue;

            interval = ComputeLabelsInterval(gl, drawRect, minValue, maxValue, labelsFont, labelsFormat, labelsSeparation);
            tmpAxisValue = minValue / interval;
            startValue = interval * Math.Ceiling(tmpAxisValue);
            if ((startValue > maxValue) || (!roundFirstValue))
                startValue = minValue;
            ticksCount = (long)Math.Floor(Math.Abs((maxValue - startValue) / interval)) + 1L;

            return ticksCount;
        }

        protected Rectangle ComputeDrawRect(OpenGL gl, float[] constValues, float minValue, float maxValue, int[] lookupTable)
        {
            if (!OpenGL.IsValidContext(gl) ||
                constValues == null || constValues.Length < 2 ||
                lookupTable == null || lookupTable.Length != 3)
                throw new ArgumentException();

            foreach (var index in lookupTable)
                if (index < 0 || index > 2)
                    throw new ArgumentOutOfRangeException();

            float[] position = new float[3];
            position[lookupTable[0]] = constValues[0];
            position[lookupTable[1]] = constValues[1];
            position[lookupTable[2]] = minValue;
            Point3D firstPoint = (Point3D)position;

            position[lookupTable[0]] = constValues[0];
            position[lookupTable[1]] = constValues[1];
            position[lookupTable[2]] = maxValue;
            Point3D secondPoint = (Point3D)position;

            firstPoint = gl.Project(firstPoint);
            secondPoint = gl.Project(secondPoint);

            Rectangle rect = new Rectangle();
            rect.X = (int)Math.Min(firstPoint.X, secondPoint.X);
            rect.Y = (int)Math.Min(firstPoint.Y, secondPoint.Y);
            rect.Width = (int)Math.Abs(firstPoint.X - secondPoint.X);
            rect.Height = (int)Math.Abs(firstPoint.Y - secondPoint.Y);

            return rect;
        }

        protected double ComputeLabelsInterval(OpenGL gl, Rectangle drawRect,
                                               double minValue, double maxValue,
                                               Font font, string format, float separation)
        {
            /*long maxLablesCount;
            long maxLableSize;
            string labelCaption;
            Size labelSize;

            if (font == null || !OpenGL.IsValidContext(gl))
                return minIncrement;

            labelCaption = minValue.ToString(format);
            labelSize = gl.GetTextSize(font, labelCaption);
            maxLableSize = labelSize.Height;
            if (maxLableSize < labelSize.Width)
                maxLableSize = labelSize.Width;

            labelCaption = maxValue.ToString(format);
            labelSize = gl.GetTextSize(font, labelCaption);
            if (maxLableSize < labelSize.Height)
                maxLableSize = labelSize.Height;
            if (maxLableSize < labelSize.Width)
                maxLableSize = labelSize.Width;

            maxLableSize += (long)Math.Ceiling((double)separation * (double)maxLableSize);

            maxLablesCount = (maxLableSize == 0 ? 1L : Math.Abs(gl.RenderContextProvider.Width / maxLableSize));

            return InternalComputeLabelsInterval(minValue, maxValue, maxLablesCount);*/

            long maxLablesCount;
            string labelCaption;
            Size labelSize, labelsMaxSize;

            if (font == null || !OpenGL.IsValidContext(gl))
                return minIncrement;

            labelCaption = minValue.ToString(format);
            labelsMaxSize = gl.GetTextSize(font, labelCaption);
            
            labelCaption = maxValue.ToString(format);
            labelSize = gl.GetTextSize(font, labelCaption);
            if (labelsMaxSize.Height < labelSize.Height)
                labelsMaxSize.Height = labelSize.Height;
            if (labelsMaxSize.Width < labelSize.Width)
                labelsMaxSize.Width = labelSize.Width;

            labelsMaxSize.Height += (int)Math.Ceiling((double)separation * (double)labelsMaxSize.Height);
            labelsMaxSize.Width += (int)Math.Ceiling((double)separation * (double)labelsMaxSize.Width);

            maxLablesCount = Math.Max(
                                (labelsMaxSize.Height == 0 ? 1L : drawRect.Height / labelsMaxSize.Height),
                                (labelsMaxSize.Width == 0 ? 1L : drawRect.Width / labelsMaxSize.Width));

            return InternalComputeLabelsInterval(minValue, maxValue, maxLablesCount);
        }

        protected double ComputeNextStep(double currentStep)
        {
            double result;

            if (currentStep >= 10.0)
                result = 10.0 * ComputeNextStep(0.1 * currentStep);
            else if (currentStep < 1.0)
                result = 0.1 * ComputeNextStep(currentStep * 10.0);
            else if (currentStep < 2.0)
                result = 2.0;
            else if (currentStep < 5.0)
                result = 5.0;
            else result = 10.0;

            return result;
        }

        protected float[] CreateTicksValuesArray(double startValue, double maxValue, double interval, long ticksCount)
        {
            double tickValue;
            long tickIndex;

            float[] ticksValues = new float[ticksCount];
            for (tickIndex = 0, tickValue = startValue; tickIndex < ticksCount; tickIndex++, tickValue += interval)
            {
                ticksValues[tickIndex] = (float)tickValue;//(_fpclass(tickValue) == _FPCLASS_NZ ? 0.0f : (float)tickValue);
            }
            /*for (tickValue = startValue, tickIndex = 0; tickValue <= maxValue; tickValue += interval, tickIndex++)
            {
                if (tickIndex < ticksCount)
                    ticksValues[tickIndex] = (float)tickValue;
            }*/

            return ticksValues;
        }

        protected Point3D[] CreateVertexBuffer(float[] constValues, float minValue, float maxValue, int[] lookupTable,
                                               float[] ticksValues, float ticksLength, Vector3D ticksDirection)
        {
            if (constValues == null || constValues.Length < 6 ||
                lookupTable == null || lookupTable.Length != 3 ||
                ticksValues == null)
                throw new ArgumentException();

            foreach (var index in lookupTable)
                if (index < 0 || index > 2)
                    throw new ArgumentOutOfRangeException();

            //List<Point3D> vertexBuffer = new List<Point3D>([2 + 2 * ticksValues.Length + 2 * ticksValues.Length);
            List<Point3D> vertexBuffer = new List<Point3D>(2 * ( 1 + 3 * ticksValues.Length));
            float[] position = new float[3];

            // Fill axis vertex buffer
            // Axis line
            position[lookupTable[0]] = constValues[0];
            position[lookupTable[1]] = constValues[1];
            position[lookupTable[2]] = minValue;
            vertexBuffer.Add((Point3D)position);

            position[lookupTable[0]] = constValues[0];
            position[lookupTable[1]] = constValues[1];
            position[lookupTable[2]] = maxValue;
            vertexBuffer.Add((Point3D)position);

            // Ticks
            foreach (var tickValue in ticksValues)
            {
                position[lookupTable[0]] = constValues[0];
                position[lookupTable[1]] = constValues[1];
                position[lookupTable[2]] = tickValue;
                vertexBuffer.Add((Point3D)position);

                position = (Point3D)position + ticksDirection * ticksLength;
                
                vertexBuffer.Add((Point3D)position);
            }
            
            // Ticks lines
            foreach (var tickValue in ticksValues)
            {
                position[lookupTable[0]] = constValues[0];
                position[lookupTable[1]] = constValues[1];
                position[lookupTable[2]] = tickValue;
                vertexBuffer.Add((Point3D)position);

                position[lookupTable[0]] = constValues[2];
                position[lookupTable[1]] = constValues[3];
                //position[lookupTable[2]] = tickValue;
                vertexBuffer.Add((Point3D)position);
            }
            
            foreach (var tickValue in ticksValues)
            {
                position[lookupTable[0]] = constValues[0];
                position[lookupTable[1]] = constValues[1];
                position[lookupTable[2]] = tickValue;
                vertexBuffer.Add((Point3D)position);

                position[lookupTable[0]] = constValues[4];
                position[lookupTable[1]] = constValues[5];
                //position[lookupTable[2]] = tickValue;
                vertexBuffer.Add((Point3D)position);
            }

            return vertexBuffer.ToArray();
        }

        protected void GetTypeDependentParameters(AxisType type, Rect3D boundingRect, float ticksLengthPercent,
                                                  out float[] constValues, out float minValue, out float maxValue, out int[] lookupTable,
                                                  out float ticksLength)
        {
            if (boundingRect.IsEmpty)
                throw new ArgumentException("Empty bounding rectangle");

            float maxDimention = boundingRect.SizeX;
            if (maxDimention < boundingRect.SizeY)
                maxDimention = boundingRect.SizeY;
            if (maxDimention < boundingRect.SizeZ)
                maxDimention = boundingRect.SizeZ;

            switch (type)
            {
                case AxisType.Abscissa:
                    minValue = boundingRect.X;
                    maxValue = boundingRect.X + boundingRect.SizeX;
                    constValues = new float[] { boundingRect.Y, boundingRect.Z,
                                                boundingRect.Y + boundingRect.SizeY, boundingRect.Z,
                                                boundingRect.Y, boundingRect.Z + boundingRect.SizeZ };
                    lookupTable = new int[] { 1, 2, 0 };
                    ticksLength = maxDimention * ticksLengthPercent;

                    break;
                
                case AxisType.Ordinate:
                    minValue = boundingRect.Y;
                    maxValue = boundingRect.Y + boundingRect.SizeY;
                    constValues = new float[] { boundingRect.X, boundingRect.Z,
                                                boundingRect.X + boundingRect.SizeX, boundingRect.Z,
                                                boundingRect.X, boundingRect.Z + boundingRect.SizeZ };
                    lookupTable = new int[] { 0, 2, 1 };
                    ticksLength = maxDimention * ticksLengthPercent;

                    break;
                
                case AxisType.Applicate:
                    minValue = boundingRect.Z;
                    maxValue = boundingRect.Z + boundingRect.SizeZ;
                    constValues = new float[] { boundingRect.X, boundingRect.Y,
                                                boundingRect.X + boundingRect.SizeX, boundingRect.Y,
                                                boundingRect.X, boundingRect.Y + boundingRect.SizeY };
                    lookupTable = new int[] { 0, 1, 2 };
                    ticksLength = maxDimention * ticksLengthPercent;

                    break;
                
                default:
                    throw new NotImplementedException("Wrong axis direction.");
            }
        }
        
        protected double InternalComputeLabelsInterval(double minValue, double maxValue, long maxLabelsCount)
        {
            long tmpLablesCount;
            double dblTmpLableCount;
            double range;
            double interval;
            bool isIntervalInf;

            range = Math.Abs(maxValue - minValue);

            if (maxLabelsCount <= 0)
            {
                if (range == 0.0)
                    return minIncrement;
                else
                    return range;
            }

            tmpLablesCount = maxLabelsCount + 1;

            if (range == 0.0)
                interval = 1.0;//s_dMinAxisIncrement;
            else
                interval = range / (double)tmpLablesCount;

            isIntervalInf = false;
            do
            {
                if (interval == 0.0)
                    interval = double.MinValue;

                dblTmpLableCount = range / interval;
                if (dblTmpLableCount < (double)long.MaxValue)
                {
                    tmpLablesCount = (long)Math.Ceiling(dblTmpLableCount);
                    if (tmpLablesCount > maxLabelsCount)
                        interval = ComputeNextStep(interval);
                }
                else
                {
                    interval = ComputeNextStep(interval);
                }
                isIntervalInf = double.IsInfinity(interval);
            }
            while ((tmpLablesCount > maxLabelsCount) && (interval < range) && (!isIntervalInf));

            if (isIntervalInf)
                interval = range;
            else
                interval = Math.Max(interval, minIncrement);

            return interval;
        }

        protected void InternalRender(OpenGL gl, Rect3D boundingRect)
        {
            if (!OpenGL.IsValidContext(gl) || boundingRect.IsEmpty)
            {
                ticksValues = null;
                verticesBuffer = null;

                return;
            }

            float[] constValues;
            float minValue;
            float maxValue;
            int[] lookupTable;
            float ticksLength;

            GetTypeDependentParameters(type, boundingRect, ticks.LengthPercent, out constValues, out minValue, out maxValue, out lookupTable, out ticksLength);
            Rectangle drawRect = ComputeDrawRect(gl, constValues, minValue, maxValue, lookupTable);

            if (this.boundingRect != boundingRect || this.drawRectSize != drawRect.Size)//this.drawRectSize.Width != gl.RenderContextProvider.Width  || this.drawRectSize.Height != gl.RenderContextProvider.Height)
            {
                double interval;
                double startValue;
                long ticksCount = ComputeAxisParameters(gl, drawRect, minValue, maxValue, ticks.RoundFirstTickValue, labels.Font, labels.Format, labels.Separation, out interval, out startValue);

                ticksValues = CreateTicksValuesArray(startValue, maxValue, interval, ticksCount);
                verticesBuffer = CreateVertexBuffer(constValues, minValue, maxValue, lookupTable, ticksValues, ticksLength, ticks.Direction);

                this.boundingRect = boundingRect;
                this.drawRectSize = drawRect.Size;//new Size(gl.RenderContextProvider.Width, gl.RenderContextProvider.Height);
            }

            if (visible)
            {
                // Render axis
                Point3D[] axisVerices = new Point3D[2];
                Array.Copy(verticesBuffer, 0, axisVerices, 0, 2);
                RenderAxis(gl, axisVerices, color, width, drawArrow);

                // Render axis ticks
                Point3D[] ticksVertices = new Point3D[2 * ticksValues.Length];
                Array.Copy(verticesBuffer, axisVerices.Length, ticksVertices, 0, ticksVertices.Length);
                ticks.Render(gl, ticksVertices);

                // Render ticks lines
                Point3D[] ticksLinesVertices = new Point3D[4 * ticksValues.Length];
                Array.Copy(verticesBuffer, axisVerices.Length + ticksVertices.Length, ticksLinesVertices, 0, ticksLinesVertices.Length);
                ticksLines.Render(gl, ticksLinesVertices);

                // Render ticks labels
                for (int index = 0; index < ticksValues.Length; index++)
                    labels.Render(gl, ticksVertices[2 * index + 1], ticksValues[index]);

                // Render axis name
                name.Render(gl, axisVerices[1]);
            }
        }

        /// <summary>
        /// Call this function to do the PropertyChanged event.
        /// </summary>
        protected virtual void NotifyPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        protected void RenderAxis(OpenGL gl,  Point3D[] vertices, GLColor color, float width, bool drawArrow)
        {
            if ((!OpenGL.IsValidContext(gl)) || (vertices == null) || (vertices.Length <= 0) || ((vertices.Length % 2) != 0))
                throw new ArgumentException();

            gl.PushAttrib(Enumerations.AttributeMask.Current | Enumerations.AttributeMask.Line | Enumerations.AttributeMask.Enable);
            {
                byte[] isLightingEnable = new byte[1];
                gl.GetBooleanv(Enumerations.GetTarget.Lighting, isLightingEnable);
                if (isLightingEnable[0] != 0)
                    gl.Enable(OpenGL.GL_COLOR_MATERIAL);

                gl.LineWidth(width);
                gl.Color(color);

                gl.Begin(Enumerations.BeginMode.Lines);
                {
                    foreach (Point3D vertex in vertices)
                        gl.Vertex(vertex);
                }
                gl.End();
            }
            gl.PopAttrib();
        }

        #endregion Protected Methods

        #region Public Constructors

        public Axis()
            : this(AxisType.Abscissa)
        {
        }

        public Axis(AxisType type)
        {
            this.type = type;

            name.Caption = DefaultName(type);
            color = name.Color = ticks.Color = labels.Color = DefaultColor(type);
            ticks.Direction = DefaultTicksDirection(type);
            DefaultTextAlignment(name, labels);

            name.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
            labels.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
            ticks.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
            ticksLines.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
        }

        #endregion Public Constructors

        #region Public Methods

        public void Render(OpenGL gl, Rect3D boundingRect)
        {
            InternalRender(gl, boundingRect);
        }

        #endregion Public Methods

        #region Public Events

        [Description("Called when properties has been changed."), Category("SharpGL")]
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion Public Events

        #region Public Properties

        [Description("Type"), Category("Axis")]
        public AxisType Type
        {
            get { return type; }
            set
            {
                if (type != value)
                {
                    type = value;

                    boundingRect = Rect3D.Empty;
                    drawRectSize = Size.Empty;

                    NotifyPropertyChanged("Type");
                }
            }
        }

        [Description("Color"), Category("Axis")]
        public GLColor Color
        {
            get { return color; }
            set
            {
                if (color != value)
                {
                    color = value;

                    NotifyPropertyChanged("Color");
                }
            }
        }

        [Description("DrawArrow"), Category("Axis")]
        public bool DrawArrow
        {
            get { return drawArrow; }
            set
            {
                if (drawArrow != value)
                {
                    drawArrow = value;

                    NotifyPropertyChanged("DrawArrow");
                }
            }
        }

        [Description("Labels"), Category("Axis")]
        public AxisLabels Labels
        {
            get { return labels; }
        }

        [Description("Name"), Category("Axis")]
        public AxisName Name
        {
            get { return name; }
        }

        [Description("Ticks"), Category("Axis")]
        public AxisTicks Ticks
        {
            get { return ticks; }
        }

        [Description("TicksLines"), Category("Axis")]
        public AxisTicksLines TicksLines
        {
            get { return ticksLines; }
        }

        [Description("Visible"), Category("Axis")]
        public bool Visible
        {
            get { return visible; }
            set
            {
                if (visible != value)
                {
                    visible = value;

                    NotifyPropertyChanged("Visible");
                }
            }
        }

        [Description("Width"), Category("Axis")]
        public float Width
        {
            get { return width; }
            set
            {
                if (width != value)
                {
                    width = value;

                    NotifyPropertyChanged("Width");
                }
            }
        }

        #endregion Public Properties
    }
}
